
/* GCSx
** IMGCHOOSE.CPP
**
** Image "chooser" popup window or control
*/

/*****************************************************************************
** Copyright (C) 2003-2006 Janson
**
** This program is free software; you can redistribute it and/or modify
** it under the terms of the GNU General Public License as published by
** the Free Software Foundation; either version 2 of the License, or
** (at your option) any later version.
** 
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
** GNU General Public License for more details.
**
** You should have received a copy of the GNU General Public License
** along with this program; if not, write to the Free Software
** Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111, USA.
*****************************************************************************/

#include "all.h"

ImageChooser::ImageChooser(TileSetEdit* myTileset) : Window() { start_func
    cursor = 0;
    dirtyRange.w = 0;
    haveFocus = 0;
    
    myFrame = NULL;
    myScroll = NULL;
    
    tileset = myTileset;
    if (tileset) tileset->markLock(); // @TODO: throw_File
    
    reloadTileStats();
}

ImageChooser::~ImageChooser() { start_func
    if (tileset) tileset->markUnlock();
}

void ImageChooser::addTo(Dialog* dialog, int showWidth, int showHeight, int cId) { start_func
    assert(!myFrame);
    assert(dialog);

    myScroll = new WidgetScroll(WidgetScroll::FRAMETYPE_BEVEL_BK, this, showWidth, showHeight, cId);
    dialog->addWidget(myScroll);
}

int ImageChooser::runModal(int xPos, int yPos) { start_func
    assert(!myScroll);

    // Prevent duplication
    if (myFrame) return 0;

    // Keep a backup cause we clear myFrame on SDL_CLOSE
    FrameWindow* myFrameBackup;
    myFrameBackup = new FrameWindow(blankString, FrameWindow::RESIZING_SNAP, FrameWindow::FRAMETYPE_BEVEL_BK, this, FrameWindow::TITLEBAR_OFF);
    myFrame = myFrameBackup;
    // Frame window can NOT delete itself- we want to do that, to ensure it isn't
    // deleted twice on exception
    myFrameBackup->setWantsToBeDeleted(0);
    
    // (10 is an approximate offset to improve visuals)
    myFrameBackup->show(xPos, max(0, yPos - height - 10), FrameWindow::SHOW_CURRENT, FrameWindow::SHOW_CURRENT);
    // Recurse into desktop event loop
    int returnValue = desktop->eventLoop();
    // Delete frame
    delete myFrameBackup;
    myFrame = NULL;
    return returnValue;
}

void ImageChooser::setDirtyBox(int begin, int end) { start_func
    if (numTiles == 0) return;

    assert(begin >= 0);
    assert(begin < numTiles);
    assert(end >= 0);
    // ('end' can be greater than number of tiles to complete a box pattern)
    assert(begin <= end);
    assert((begin % tilesPerLine) <= (end % tilesPerLine));

    // Create a rectangle, include outermost gridlines
    Rect range;
    range.x = (begin % tilesPerLine) * tileWidth;
    range.y = (begin / tilesPerLine) * tileHeight;
    range.w = (end % tilesPerLine - begin % tilesPerLine + 1) * tileWidth;
    range.h = (end / tilesPerLine - begin / tilesPerLine + 1) * tileHeight;
    
    // Add rectangle into dirty range
    boundRects(dirtyRange, range);
    setDirty();
}

void ImageChooser::setDirtyRange(int first, int last) { start_func
    if (numTiles == 0) return;

    assert(first >= 0);
    assert(first < numTiles);
    assert(last >= 0);
    assert(last < numTiles);
    
    if (first > last) swap(first, last);

    // On the same line?
    if (first / tilesPerLine == last / tilesPerLine) {
        setDirtyBox(first, last);
    }
    else {
        // Have to cover entire lines
        setDirtyBox(first - first % tilesPerLine, last - last % tilesPerLine + tilesPerLine - 1);
    }
}

void ImageChooser::setTileset(TileSetEdit* newTileset) { start_func
    if (tileset) {
        tileset->markUnlock();
        tileset = NULL;
    }
    if (newTileset) {
        tileset = newTileset;
        tileset->markLock(); // @TODO: throw_File
        reloadTileStats();
    }
    else {
        setDirty(1);
    }
}

void ImageChooser::reloadTileStats() { start_func
    if (tileset) {
        numTiles = tileset->getCount();
        tileWidth = tileset->getWidth();
        tileHeight = tileset->getHeight();
        tilesPerLine = tileset->getTilesPerLine();
        fixedTilesPerLine = tilesPerLine ? 1 : 0;
    
        // (never put cursor below 0)
        if (numTiles) {
            if (cursor >= numTiles) cursor = numTiles - 1;
        }
    }
    else {
        cursor = tilesPerLine = fixedTilesPerLine = numTiles = 0;
        tileWidth = tileHeight = 1; // Prevents potential div/0
    }
    
    resize(width, height);
    setDirty(1);
}

void ImageChooser::resize(int newWidth, int newHeight, int newViewWidth, int newViewHeight, int fromParent) { start_func
    if (newViewWidth == -1) newViewWidth = viewWidth;

    // Calculate tiles per line, if not fixed
    if (!fixedTilesPerLine) {
        // For non fixed tiles, default width is 3/4ths desktop
        if (newViewWidth <= 0) newViewWidth = desktop->desktopWidth() * 3 / 4;
        tilesPerLine = newViewWidth / tileWidth;
        if (tilesPerLine < 1) tilesPerLine = 1;
    }

    // Size to match
    int myWidth = tilesPerLine * tileWidth;
    int myHeight = numTiles / tilesPerLine;
    if (numTiles % tilesPerLine) ++myHeight;
    myHeight = myHeight * tileHeight;
    
    // Recurse back to parent until we get a match
    if ((myWidth != width) || (myHeight != height)) fromParent = 0;    
    Window::resize(myWidth, myHeight, newViewWidth, newViewHeight, fromParent);
    if (myFrame) myFrame->setScroll(tileWidth, tileHeight);
    if (myScroll) myScroll->setScroll(tileWidth, tileHeight);
}

int ImageChooser::event(int hasFocus, const SDL_Event* event) { start_func
    int key;
    int target;
    int changed = 0;
    ObjChange* obj;

    switch (event->type) {
        case SDL_CLOSE:
            myFrame = NULL;
            myScroll = NULL;
            return 1;

        case SDL_OBJECTCHANGE:
            obj = (ObjChange*)event->user.data1;
        
            if ((event->user.code & OBJ_TILESET) && (obj->obj == tileset)) {
                if (event->user.code & OBJMOD_DELETE) {
                    tileset = NULL;
                    closeWindow();
                }
                // @TODO: OBJMOD_COUNTCOLL- don't need to reload tile stats,
                // but other stuff will need reloading once we can edit collisions
                if (event->user.code & (OBJMOD_WIDTH | OBJMOD_HEIGHT | OBJMOD_COUNT | OBJMOD_PERLINE)) {
                    reloadTileStats();
                }
                if (event->user.code & OBJMOD_TILE) {
                    setDirtyRange(obj->info1 - 1, obj->info2 - 1);
                }
            }
            if ((event->user.code & OBJ_WORLD) && (tileset) && (obj->obj == tileset->getWorld())) {
                if (event->user.code & OBJMOD_DELETE) {
                    tileset = NULL;
                    closeWindow();
                }
            }
            return 1;
    
        case SDL_MOUSEBUTTONDOWN:
        case SDL_MOUSEBUTTONDBL:
            if (event->button.button == SDL_BUTTON_LEFT) {
                int mX = event->button.x / tileWidth;
                int mY = event->button.y / tileHeight;
                
                // (clip to edge, in cse off of it)
                if (mX >= tilesPerLine) mX = tilesPerLine - 1;
                if (mY >= (numTiles + tilesPerLine - 1) / tilesPerLine) mY = (numTiles + tilesPerLine - 1) / tilesPerLine - 1;
                
                cursorState(mX + mY * tilesPerLine);
                return 1;
            }
            break;
            
        case SDL_MOUSEBUTTONUP:
            if (event->button.button == SDL_BUTTON_LEFT) {
                if (myFrame) {
                    desktop->setModalReturn(cursor + 1);
                    closeWindow();
                }
                return 1;
            }
            break;
    
        case SDL_MOUSEMOTION:
            if (event->motion.state & SDL_BUTTON_LMASK) {
                int mX = ((Sint16)event->motion.x) / tileWidth;
                int mY = ((Sint16)event->motion.y) / tileHeight;
                
                // (clip to edge, in case off of it)
                if (mX < 0) mX = 0;
                if (mY < 0) mY = 0;
                if (mX >= tilesPerLine) mX = tilesPerLine - 1;
                if (mY >= (numTiles + tilesPerLine - 1) / tilesPerLine) mY = (numTiles + tilesPerLine - 1) / tilesPerLine - 1;
                
                cursorState(mX + mY * tilesPerLine);
            }
            return 1;

        case SDL_SPECIAL:
            // Refresh cursor color?
            if ((event->user.code == SDL_IDLECURSOR) && (haveFocus)) {
                setDirtyRange(cursor, cursor);
            }
            return 1;

        case SDL_INPUTFOCUS:
            if (event->user.code & 1) {
                if (!haveFocus) {
                    haveFocus = 1;
                    changed = 1;
                }
            }
            if (!(event->user.code & 1)) {
                if (haveFocus) {
                    haveFocus = 0;
                    changed = 1;
                }
            }
            
            if (changed) {
                setDirtyRange(cursor, cursor);
            }
            return 1;            

        case SDL_KEYDOWN:
            key = combineKey(event->key.keysym.sym, event->key.keysym.mod & ~KMOD_SHIFT);
        
            switch (key) {
                case SDLK_ESCAPE:
                    if (myFrame) {
                        desktop->setModalReturn(0);
                        closeWindow();
                    }
                    return 1;

                case SDLK_KP_ENTER:
                case SDLK_RETURN:
                case SDLK_SPACE:
                    if (myFrame) {
                        desktop->setModalReturn(cursor + 1);
                        closeWindow();
                    }
                    return 1;
            
                case SDLK_RIGHT:
                    cursorState(cursor + 1);
                    return 1;
                        
                case combineKey(SDLK_RIGHT, KMOD_CTRL):
                case SDLK_END:
                    cursorState(cursor + tilesPerLine - (cursor % tilesPerLine) - 1);
                    return 1;
                        
                case SDLK_LEFT:
                    if (cursor > 0) cursorState(cursor - 1);
                    return 1;

                case combineKey(SDLK_LEFT, KMOD_CTRL):
                case SDLK_HOME:
                    cursorState(cursor - (cursor % tilesPerLine));
                    return 1;
                        
                case SDLK_DOWN:
                    cursorState(cursor + tilesPerLine);
                    return 1;

                case SDLK_PAGEDOWN:
                    cursorState(cursor + (viewHeight / tileHeight - 1) * tilesPerLine);
                    return 1;

                case SDLK_PAGEUP:
                    target = cursor - (viewHeight / tileHeight - 1) * tilesPerLine;
                    if (target > 0) cursorState(target);
                    else cursorState(0);
                    return 1;

                case SDLK_UP:
                    if (cursor - tilesPerLine > 0) cursorState(cursor - tilesPerLine);
                    else cursorState(0);
                    return 1;

                case combineKey(SDLK_HOME, KMOD_CTRL):
                    cursorState(0);
                    return 1;

                case combineKey(SDLK_END, KMOD_CTRL):
                    cursorState(numTiles - 1);
                    return 1;
            }
            break;
    }
    return 0;
}

int ImageChooser::cursorState(int newPos) { start_func
    if (newPos >= 0) {
        // Clip on right
        if ((newPos >= numTiles) && (numTiles)) newPos = numTiles - 1;
        
        // Dirty previous selection
        setDirtyRange(cursor,cursor);
        cursor = newPos;

        // Scroll?
        int newY = cursor / tilesPerLine * tileHeight;
        int newX = (cursor % tilesPerLine) * tileWidth;
        if (myFrame) myFrame->scrollToView(newX, newY, tileWidth, tileHeight);
        if (myScroll) myScroll->scrollToView(newX, newY, tileWidth, tileHeight);

        // Dirty
        setDirtyRange(cursor,cursor);
    }
    
    return cursor;
}

void ImageChooser::display(SDL_Surface* destSurface, Rect& toDisplay, const Rect& clipArea, int xOffset, int yOffset) { start_func
    assert(destSurface);

    if (visible) {
        // If dirty, redraw range or all
        if (dirty) {
            if (totalDirty) {
                getRect(toDisplay);
                toDisplay.x += xOffset;
                toDisplay.y += yOffset;
            }
            else {
                dirtyRange.x += x + xOffset;
                dirtyRange.y += y + yOffset;
                // Range must include requested update area as well
                boundRects(toDisplay, dirtyRange);
            }
            dirty = totalDirty = 0;
            dirtyRange.w = 0;
            intersectRects(toDisplay, clipArea);
        }
        
        xOffset += x;
        yOffset += y;
        
        // Anything to draw?
        if (toDisplay.w) {
            SDL_SetClipRect(destSurface, &toDisplay);
            SDL_FillRect(destSurface, &toDisplay, guiPacked[COLOR_BKFILL]);
            
            if (!tileset) return;

            int dX = xOffset;
            int lastX = xOffset + width;
            
            // Draw starting at the first line of the displayed area;
            int skip = (toDisplay.y - yOffset) / tileHeight;

            int dY = yOffset + skip * tileHeight;
            int lastY = toDisplay.y + toDisplay.h;
            int pos = tilesPerLine * skip;

            // Determine first/last tiles of each row we need to show
            // These numbers would be, for example, 0 for first tile, 1 for second,
            // up to tilesPerLine - 1
            int leftmost = (toDisplay.x - xOffset) / tileWidth;
            int rightmost = (toDisplay.x + toDisplay.w - xOffset - 1) / tileWidth;
            
            pos += leftmost;
            dX += tileWidth * leftmost;
            
            for (; (pos < numTiles) && (dY < lastY); ++pos) {
                int posMod = pos % tilesPerLine;
            
                // We use modulo to determine if this tile needs displaying
                if ((posMod >= leftmost) && (posMod <= rightmost)) {
                    // Draw tile
                    tileset->blitTile(pos + 1, destSurface, dX, dY);
                    
                    if (pos == cursor) {
                        drawSelectRect(dX, dY, tileWidth, tileHeight,
                                       guiPacked[haveFocus ? COLOR_TILECURSOR : COLOR_TILESELECTION], destSurface,
                                       haveFocus ? desktop->currentCursorAlpha() : 128);

                        drawBox(dX, dY, tileWidth, tileHeight, guiPacked[COLOR_TILECURSORBORDER1], destSurface);
                        drawBox(dX + 1, dY + 1, tileWidth - 2, tileHeight - 2, guiPacked[COLOR_TILECURSORBORDER2], destSurface);
                    }
                }
                    
                dX += tileWidth;
                if (dX + tileWidth > lastX) {
                    dX = xOffset;
                    dY += tileHeight;
                }
            }
        }
    }
}

Window::WindowType ImageChooser::windowType() const { start_func
    return WINDOW_CLIENT;
}

Window::WindowSort ImageChooser::windowSort() const { start_func
    if (myScroll) return WINDOWSORT_NORMAL;
    return WINDOWSORT_MODAL;
}

int ImageChooser::wantsToBeDeleted() const { start_func
    if (myScroll) return 1;
    return 0;
}

